package pl.com.qapps.unity;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Closeable;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import pl.com.qapps.unity.Quicklist.Action;
import pl.com.qapps.unity.Quicklist.Item;
import pl.com.qapps.unity.Quicklist.Radio;
import pl.com.qapps.unity.Quicklist.RadioGroup;
import pl.com.qapps.unity.Quicklist.Select;
import pl.com.qapps.unity.UnityPb.ItemPb;
import pl.com.qapps.unity.UnityPb.ItemPb.Type;
import pl.com.qapps.unity.UnityPb.LauncherProperty;
import pl.com.qapps.unity.UnityPb.PropertyValue;
import pl.com.qapps.unity.UnityPb.QuicklistPb;

/**
 * Represents the launcher of single application in Unity launcher. <br>
 * {@link UnityLauncher} objects are ThreadSafe. <br>
 * All methods can throw {@linkplain LauncherException} if any unexpected error
 * occurs, which is runtime exception due to assumption that it happens really
 * rarely, if ever, most likely caused by implementation oversight.
 * 
 * @author Jakub Dykowski
 */
public final class UnityLauncher implements Closeable {

	/*
	 * some sort of cache holding launcher instances, where key is
	 * '<app_name>.desktop'
	 */
	private static final Map<String, UnityLauncher> launchers = new WeakHashMap<>();

	/**
	 * The app name should be a '.desktop' file name that exists on the system.
	 * 
	 * @param appName
	 *            should ends with '.desktop'
	 * @return
	 * @throws AppNotFoundException
	 *             if given app does not exists
	 * @throws LauncherException
	 *             if any internal error occurred
	 */
	public static UnityLauncher get(String appName)
			throws AppNotFoundException, LauncherException {

		synchronized (launchers) {
			UnityLauncher cached = launchers.get(appName);
			if (cached == null) {
				launchers.put(appName, cached = new UnityLauncher(appName));
			}
			return cached;
		}
	}

	static Collection<UnityLauncher> getRegisteredLaunchers() {
		synchronized (launchers) {
			return launchers.values();
		}
	}

	private final LauncherProxy proxy;
	private boolean closed = false;
	private Quicklist quicklist;
	private List<ItemStateUpdater> stateUpdaters = new LinkedList<>();

	private UnityLauncher(String appName) throws AppNotFoundException,
			LauncherException {

		proxy = new LauncherProxy(appName);
	}

	/**
	 * Return the application name to to which this {@link UnityLauncher}
	 * belongs to. It is the file name which ends with '.desktop'
	 * 
	 * @return the application name
	 */
	public String getAppName() {
		return proxy.getAppName();
	}

	/**
	 * Looks like: 'application://{@linkplain #getAppName()}'
	 * 
	 * @return the application uri
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public String getAppUri() throws LauncherException {
		return proxy.get(LauncherProperty.APP_URI).getString();
	}

	/**
	 * Notice that it may be not visible. {@link #setCountVisible(boolean)}
	 * 
	 * @return the number displayed in right upper-right corner of the launcher
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public int getCount() throws LauncherException {
		return proxy.get(LauncherProperty.COUNT).getInt();
	}

	/**
	 * To be displayed you may need to call {@link #setCountVisible(boolean)}
	 * first.
	 * 
	 * @param count
	 *            the number displayed in right upper-right corner of the
	 *            launcher, has to be at least 0
	 * @return the {@link UnitLauncher} itself
	 * @throws IllegalArgumentException
	 *             if count is < than 0
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setCount(int count) throws IllegalArgumentException,
			LauncherException {
		if (count < 0)
			throw new IllegalArgumentException("count has to be >= 0 but "
					+ count + " given");
		proxy.set(LauncherProperty.COUNT,
				PropertyValue.newBuilder().setInt(count).build());
		return this;
	}

	/**
	 * Checks if the count number is visible.
	 * 
	 * @return <code>true</code> if visible, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isCountVisible() throws LauncherException {
		return proxy.get(LauncherProperty.COUNT_VISIBLE).getBool();
	}

	/**
	 * Sets whether the count number should be visible or not.
	 * 
	 * @param countVisible
	 *            <code>true</code> if visible <code>false</code> otherwise
	 * @return the {@link UnitLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setCountVisible(boolean countVisible)
			throws LauncherException {
		proxy.set(LauncherProperty.COUNT_VISIBLE, PropertyValue.newBuilder()
				.setBool(countVisible).build());
		return this;
	}

	/**
	 * Checks the progress status which may or may not be visible on the
	 * launcher.
	 * 
	 * @return progress status, a number between 0 and 1 (both inclusive)
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public double getProgress() throws LauncherException {
		return proxy.get(LauncherProperty.PROGRESS).getDouble();
	}

	/**
	 * Sets the progress status which may or may not be visible ont the
	 * launcher.
	 * 
	 * @param progress
	 *            a number between 0 and 1 (both inclusive)
	 * @return the {@link UnitLauncher} itself
	 * @throws IllegalArgumentException
	 *             if progress is less than 0 or grower than 1
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setProgress(double progress)
			throws IllegalArgumentException, LauncherException {
		if (progress < 0 || progress > 1) {
			throw new IllegalArgumentException(
					"progress has to be < 1 and > 0 but " + progress + " given");
		}
		proxy.set(LauncherProperty.PROGRESS, PropertyValue.newBuilder()
				.setDouble(progress).build());
		return this;
	}

	/**
	 * Checks whether the progress status is visible.
	 * 
	 * @return <code>true</code> if visible, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isProgressVisible() throws LauncherException {
		return proxy.get(LauncherProperty.PROGRESS_VISIBLE).getBool();
	}

	/**
	 * Sets whether the progress status should be visible or not.
	 * 
	 * @param progressVisible
	 *            <code>true</code> if visible, <code>false</code> otherwise
	 * @return the {@link UnitLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setProgressVisible(boolean progressVisible)
			throws LauncherException {
		proxy.set(LauncherProperty.PROGRESS_VISIBLE, PropertyValue.newBuilder()
				.setBool(progressVisible).build());
		return this;
	}

	/**
	 * Checks whether launcher is set to be urgent or not. Urgency means that it
	 * will be attracting user attention.
	 * 
	 * @return <code>true</code> if is urgent, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isUrgent() throws LauncherException {
		return proxy.get(LauncherProperty.URGENT).getBool();
	}

	/**
	 * Sets whether the launcher should be marked as urgent or not.
	 * 
	 * @param urgent
	 *            <code>true</code> if urgent, <code>false</code> otherwise
	 * @return the {@link UnitLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setUrgent(boolean urgent) throws LauncherException {
		proxy.set(LauncherProperty.URGENT,
				PropertyValue.newBuilder().setBool(urgent).build());
		return this;
	}

	/**
	 * Returns the previously assigned quicklist or <code>null</code> if there
	 * is none. The default value is <code>null</code>.
	 * 
	 * @return quicklist or <code>null</code> if none
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public Quicklist getQuicklist() {
		return this.quicklist;
	}

	private Lock quicklistLock = new ReentrantLock();

	/**
	 * Sets the quicklist available after right click one the Unity launcher. If
	 * there where previous one it gets replaced.
	 * 
	 * @param quicklist
	 *            <code>null</code> are acceptable either as empty quicklists
	 * @return
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setQuicklist(Quicklist quicklist)
			throws LauncherException {
		quicklistLock.lock();
		try {
			// remove all previous update listeners and unregister actions
			if (this.quicklist != null) {
				for (ItemStateUpdater updater : stateUpdaters) {
					updater.item.removePropertyChangeListener(updater);
					Unity.unregisterAction(updater.item.getId());
				}
			}
			stateUpdaters.clear();

			// TODO consider behavior when quicklist is NULL
			if (quicklist != null) {

				QuicklistPb.Builder quicklistB = QuicklistPb.newBuilder();
				for (final Item item : quicklist) {

					Type itemType = Quicklist.getItemType(item);
					ItemPb.Builder itemBuilder = ItemPb.newBuilder()
							.setId(item.getId()).setEnabled(item.isEnabled())
							.setType(itemType).setVisible(item.isVisible());

					if (item.getLabel() != null)
						itemBuilder.setLabel(item.getLabel());

					switch (itemType) {
					case SELECT:
						Select select = (Select) item;
						itemBuilder.setSelected(select.isSelected());
						break;
					case RADIO:
						@SuppressWarnings("rawtypes")
						Radio radio = (Radio) item;
						// TODO remove groupid leftovers
						// itemBuilder.setGroup(radio.getGroup().getId());
						itemBuilder.setSelected(radio.isSelected());
					}
					quicklistB.addItem(itemBuilder.build());
				}

				// actually set the quicklist
				proxy.set(LauncherProperty.QUICKLIST, PropertyValue
						.newBuilder().setQuicklist(quicklistB.build()).build());

				// synchronize items state with the real quicklist items
				for (Item item : quicklist) {

					Unity.registerAction(item.getId(), item.getAction());

					ItemStateUpdater updater = new ItemStateUpdater(item);
					stateUpdaters.add(updater);
					item.addPropertyChangeListener(updater);
				}

				this.quicklist = quicklist;
			}
		} finally {
			quicklistLock.unlock();
		}
		return this;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof UnityLauncher))
			return false;
		UnityLauncher other = (UnityLauncher) obj;
		return getAppName().equals(other.getAppName());
	}

	@Override
	public int hashCode() {
		return getAppName().hashCode();
	}

	// TODO consider keeping exposed close() functionality and how it would look
	// like
	/**
	 * Use it to allow the garbage collector to free any resources used by this
	 * object.
	 */
	@Override
	public synchronized void close() {
		System.out.println("closing " + this + " already closed: " + closed);
		if (!closed) {
			closed = true;
			synchronized (launchers) {

				launchers.remove(getAppName());
				quicklist = null;

				// clear all listeners
				quicklistLock.lock();
				try {
					for (ItemStateUpdater updater : stateUpdaters) {
						updater.item.removePropertyChangeListener(updater);
						Unity.unregisterAction(updater.item.getId());
					}
					stateUpdaters.clear();
					stateUpdaters = null;
				} finally {
					quicklistLock.unlock();
				}
			}
		}
	}

	@Override
	protected void finalize() throws Throwable {
		close();
		super.finalize();
	}

	@Override
	public String toString() {
		return "<" + getClass().getSimpleName() + " " + getAppName() + ">";
	}

	/**
	 * Updates the state of item (visible, enabled, label etc.) so the state of
	 * real item is synchronized.
	 * 
	 * @author Jakub Dykowski
	 * 
	 */
	private class ItemStateUpdater implements PropertyChangeListener {

		private final Item item;

		private ItemStateUpdater(Item item) {
			if (item == null)
				throw new NullPointerException("item cannot be null");
			this.item = item;
		}

		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			ItemPb.Builder builder = ItemPb.newBuilder().setId(item.getId());

			switch (evt.getPropertyName()) {
			case "label":
				builder.setLabel(evt.getNewValue().toString());
				break;
			case "visible":
				builder.setVisible((boolean) evt.getNewValue());
				break;
			case "enabled":
				builder.setEnabled((boolean) evt.getNewValue());
				break;
			case "selected":
				builder.setSelected((boolean) evt.getNewValue());
				break;
			default:
				return;
			}
			proxy.update(builder.build());
		}

	}

	public static void main(String[] args) throws AppNotFoundException,
			InterruptedException, LauncherException {

		final UnityLauncher eclipse = UnityLauncher.get("Eclipse.desktop");
		eclipse.setUrgent(true);
		Quicklist ql = new Quicklist();

		Select countVisible = new Select("Count visible") {

			@Override
			public void selected(boolean selected) {
				eclipse.setCountVisible(selected);
			}

		};
		ql.add(countVisible);

		Select progressVisible = new Select("Progress visible") {

			@Override
			public void selected(boolean selected) {
				eclipse.setProgressVisible(selected);
			}

		};
		ql.add(progressVisible);

		Select urgent = new Select("Urgent", eclipse.isUrgent()) {

			@Override
			public void selected(boolean selected) {
				eclipse.setUrgent(selected);
			}

		};
		ql.add(urgent);
		Action incrCount = new Action("Count++", new Runnable() {

			@Override
			public void run() {
				eclipse.setCount(eclipse.getCount() + 1);
			}

		});
		ql.add(incrCount);
		eclipse.setQuicklist(ql);

		Quicklist q = new Quicklist();

		RadioGroup<String> state = new RadioGroup<String>() {

			@Override
			public void selected(String option) {
				System.out.println("RadioGroup state: " + option);
			}

		};
		Radio<String> on = new Radio<>("On", state, "ON");
		Radio<String> off = new Radio<>("Off", state, "OFF");
		q.add(on).add(off).add(Quicklist.SEPARATOR);

		q.add(new Action("Label nr 1", null));
		q.add(new Action("Label nr 2", new Runnable() {

			public void run() {
				System.out.println("Label nr has been activated!!");
			}

		}));
		Select cb = new Select("This is CheckBox", false) {

			@Override
			public void selected(boolean selected) {
				System.out.println("Checkbox is selected: " + selected);
			}

		};
		q.add(cb);

		UnityLauncher kadu = UnityLauncher.get("kadu.desktop");
		kadu.setQuicklist(q);

		kadu.setUrgent(true);
		kadu.setCount(99);
		kadu.setCountVisible(true);
		kadu.setProgress(0.5);
		kadu.setProgressVisible(true);

		TimeUnit.SECONDS.sleep(2);

		kadu.setCount(3);
		kadu.setProgress(0.75);
		kadu.setUrgent(false);

		System.out.println(kadu.isUrgent());
		System.out.println(kadu.isCountVisible());
		System.out.println(kadu.isProgressVisible());
		System.out.println(kadu.getAppName());
		System.out.println(kadu.getAppUri());
		System.out.println(kadu.getCount());
		System.out.println(kadu.getProgress());
		System.err.println("Everything done!!!!!!!!!!!!!!!");

		cb.setSelected(true);
		cb.setLabel("Just a Checkbox");
		// kadu.setQuicklist(q);
		// kadu.setQuicklist(q);
	}
}
